package simpleftp.client.control;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.prefs.BackingStoreException;
import java.util.prefs.Preferences;

import javax.swing.JFileChooser;
import javax.swing.JOptionPane;
import javax.swing.border.EtchedBorder;
import javax.swing.border.TitledBorder;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreePath;

import simpleftp.client.gui.FTPClientWindow;

public class CommandDispatcher implements Runnable 
{
	public static final String NL = "\n";
	private static final String USER = "USER";
	private static final String PASS = "PASS";
	private static final String SERVER = "SERVER";
	private static final String PORT = "PORT";
	private static final String SETTINGS_ROOT = "simpleftpsettings";
	private static final String DEFAULT_SETTINGS = SETTINGS_ROOT + "/default";
	private static final String LAST_SETTINGS = SETTINGS_ROOT + "/last";
	private static final String DEF_USER = "paris";
	private static final String DEF_PASS = "paris";
	private static final String DEF_SERVER = "127.0.0.1";
	private static final String DEF_PORT = "7802";
	private final ExecutorService executorPool;
//	private final CompletionService<Boolean> completionPool; // Updated: not needed anymore since we use the ExecutorService
	private final FTPClientWindow window;
	private final int numberOfCores;
	private final double blockingCoefficient = 0.9;
	private final int poolSize;
	private BlockingQueue<Command> commands;
	private SimpleFTP client;
	private Preferences userPrefs;
	private String server;
	private String port;
	private String user;
	private String pass;
	private boolean alreadyConnected;

	public CommandDispatcher(BlockingQueue<Command> commandQueue, FTPClientWindow window) {
		
		this.commands = commandQueue;
		this.window = window;
		alreadyConnected = false;
		numberOfCores = Runtime.getRuntime().availableProcessors();
		poolSize = (int) (numberOfCores / (1 - blockingCoefficient));
		executorPool = Executors.newFixedThreadPool(poolSize);
//		completionPool = new ExecutorCompletionService<Boolean>(executorPool); // Updated. Not needed anymore.
		client = new SimpleFTP();
	}

	@Override
	public void run() {
		
		try {
			loadStartupPreferences();
			Command cmd = this.commands.take();
			while (!cmd.equals(Command.QUIT)) {
				switch (cmd) {
					case CONNECT:
						connect();
						break;
						
					case DISCONNECT:
						disconnect();
						break;
						
					case STOR:
						try {
							upload();
						} catch (ExecutionException e) {
							e.printStackTrace();
						}
						break;
						
					case LIST:
						listDir();
						break;
						
					case RETR:
						download();
						break;
						
					case PWD:
						pwd();
						break;
					
					case CWD:
						cwd();
						break;
						
					case CLEAR:
						clearConsole();
						break;
						
					case LOADPREFS:
						loadPreferences();
						break;
						
					case SAVEPREFS:
						savePreferences();
						break;
						
					case DELPREF:
						deletePreference();
						break;
						
					default:
						break;
				}
				cmd = this.commands.take();
			}
			saveLastPreferences();
			disconnect();
			executorPool.shutdown();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}

	private void clearConsole() {
		window.console.setText("");
	}

	private void saveLastPreferences() {
		userPrefs = Preferences.userRoot().node(LAST_SETTINGS);
		userPrefs.put(USER, window.tfusername.getText());
		userPrefs.put(PASS, new String(window.pfuserpass.getPassword()));
		userPrefs.put(SERVER, window.tfservername.getText());
		userPrefs.put(PORT, window.tfport.getText());
		try {
			userPrefs.flush();
		} catch (BackingStoreException e) {
			e.printStackTrace();
		}
	}

	private void loadStartupPreferences() {
		try {
			userPrefs = Preferences.userRoot();
			if (!userPrefs.nodeExists(DEFAULT_SETTINGS)) {
				userPrefs = Preferences.userRoot().node(DEFAULT_SETTINGS);
				userPrefs.put(USER, DEF_USER);
				userPrefs.put(PASS, DEF_PASS);
				userPrefs.put(SERVER, DEF_SERVER);
				userPrefs.put(PORT, DEF_PORT);
				userPrefs.flush();
			}
			if (userPrefs.nodeExists(LAST_SETTINGS)) 
				userPrefs = Preferences.userRoot().node(LAST_SETTINGS);
			 
			window.tfusername.setText(userPrefs.get(USER, DEF_USER));
			window.pfuserpass.setText(userPrefs.get(PASS, DEF_PASS));
			window.tfservername.setText(userPrefs.get(SERVER, DEF_SERVER));
			window.tfport.setText(userPrefs.get(PORT, DEF_PORT));
		} catch (BackingStoreException e) {
			e.printStackTrace();
		}
	}
	
	private void loadPreferences() {
		try {
			userPrefs = Preferences.userRoot().node(SETTINGS_ROOT);
			String[] userSavedPrefs = userPrefs.childrenNames();
			String s = (String) JOptionPane.showInputDialog(window,
					"Choose preference to load:", "Load Preferences",
					JOptionPane.QUESTION_MESSAGE, null, userSavedPrefs, userSavedPrefs[0]);
			if ((s != null) && (s.length() > 0)) {
				userPrefs = Preferences.userRoot().node(SETTINGS_ROOT + "/" + s);
				window.tfusername.setText(userPrefs.get(USER, DEF_USER));
				window.pfuserpass.setText(userPrefs.get(PASS, DEF_PASS));
				window.tfservername.setText(userPrefs.get(SERVER, DEF_SERVER));
				window.tfport.setText(userPrefs.get(PORT, DEF_PORT));
				
				window.console.append(NL + "User preferences " + s + " loaded.");
			}
			else 
				window.console.append(NL + "Preferences loading operation cancelled.");
			
		} catch (BackingStoreException e) {
			e.printStackTrace();
		}
	}

	private void savePreferences() {
		String s = (String) JOptionPane.showInputDialog(window,
				"Save the current connection info as:", "Save Preferences",
				JOptionPane.QUESTION_MESSAGE, null, null, null);
		if ((s != null) && (s.length() > 0)) {
			userPrefs = Preferences.userRoot().node(SETTINGS_ROOT + "/" + s);
			userPrefs.put(USER, window.tfusername.getText());
			userPrefs.put(PASS, new String(window.pfuserpass.getPassword()));
			userPrefs.put(SERVER, window.tfservername.getText());
			userPrefs.put(PORT, window.tfport.getText());
			try {
				userPrefs.flush();
			} catch (BackingStoreException e) {
				window.console.append(NL + "Could not save preferences: "
						+ e.getMessage());
				e.printStackTrace();
			}
			window.console.append(NL + "Preferences saved as " + s);
		} 
		else 
			window.console.append(NL + "Preferences saving operation cancelled.");
		
	}
	
	private void deletePreference() {
		try {
			userPrefs = Preferences.userRoot().node(SETTINGS_ROOT);
			String[] userSavedPrefs = userPrefs.childrenNames();
			String s = (String) JOptionPane.showInputDialog(window,
					"Choose preference to delete:", "Delete Preference",
					JOptionPane.QUESTION_MESSAGE, null, userSavedPrefs, userSavedPrefs[0]);
			if ((s != null) && (s.length() > 0)) {
				userPrefs = Preferences.userRoot().node(SETTINGS_ROOT + "/" + s);
				userPrefs.removeNode();
				userPrefs.flush();
				window.console.append(NL + "User preferences " + s + " deleted.");
			}
			else 
				window.console.append(NL + "Preferences deletion operation cancelled.");	
		} catch (BackingStoreException e) {
			e.printStackTrace();
		}
	}
	
	/**
	 * Added method. Download a file from Server.
	 */
	private void download() {
		if (!alreadyConnected) {
			window.console.append(NL + "You are not connected to any server.");
			return;
		}
		final JFileChooser fc = new JFileChooser();
		fc.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
		int returnVal = fc.showDialog(window, "Download directory");
		if (returnVal == JFileChooser.APPROVE_OPTION) {
			// File download
			TreePath[] paths = window.tree.getSelectionPaths();
			for (TreePath path : paths) {
				String filePath = "";
				Object[] pathComponents = path.getPath();
				for (Object component : pathComponents)
					filePath += component+"/";
				File fileToDL = new File(filePath);
				
				executorPool.submit(new Downloader(fileToDL, fc.getSelectedFile(), window, server, port, user, pass));
			}
		}
		else
			window.console.append(NL + "Upload action cancelled.");
	}
	

	private void upload() throws InterruptedException, ExecutionException {
		if (!alreadyConnected) {
			window.console.append(NL + "You are not connected to any server.");
			return;
		}
		final JFileChooser fc = new JFileChooser();
		fc.setFileSelectionMode(JFileChooser.FILES_ONLY);
		fc.setMultiSelectionEnabled(true);
		fc.setDragEnabled(true);
		int returnVal = fc.showDialog(window, "Upload");
		if (returnVal == JFileChooser.APPROVE_OPTION) {				
			// Added. Modified the way we upload files, so we have a Boolean to check for each file if the upload
			// is successfull or not.
			File[] files = fc.getSelectedFiles(); // From the JFileChooser
			List<Callable<Boolean>> uploaders = new ArrayList<Callable<Boolean>>(files.length);
			
			for (File file : files)
				uploaders.add(new Uploader(file, window, server, port, user, pass));
			
			final List<Future<Boolean>> resultOfUploads = executorPool.invokeAll(uploaders);
			
			int i=0;
			for (final Future<Boolean> result : resultOfUploads) {
				if (result.get()) {  // Adding in live the node if the upload was successful.
					DefaultMutableTreeNode childNode = new DefaultMutableTreeNode(((Uploader) uploaders.get(i)).file.getName()); 
					window.root.add(childNode); // Update the JTree, because we are ALWAYS uploading to the current working directory (PWD)
				}
				i++;
			}
			
			window.tree.updateUI();
		}
		else
			window.console.append(NL + "Upload action cancelled.");
	}

	private void listDir() {
		if (!alreadyConnected) {
			window.console.append(NL + "You are not connected to any server.");
			return;
		}
		try {
// Old stuff.
//			String dirContent = client.ls();
//			window.console.append(NL + dirContent);
//			System.out.println(f.toString());
			
			client.ls(); // Sends the LIST comment to the FTPServer and place it in a client attribute.
			File f = client.getFileTree();
			window.root = new DefaultMutableTreeNode(f.getName()); // New root is created with root dir's name
			// The top node to go to previous dir. Ok this is not the safier way to do it but lack of time to implement a correct history of pre-visited directories.
			DefaultMutableTreeNode precDirNode = new DefaultMutableTreeNode("..");
			window.root.add(precDirNode);
			populateTree(window.root, f); // We populate the tree on the client-side...
			window.tree.setModel(new DefaultTreeModel(window.root)); // Update the JTree model.
			window.tree.updateUI(); // And update the JTree's UI.
		} catch (IOException e) {
			window.console.append(NL + e.getMessage());
			e.printStackTrace();
		}
	}
	
	/**
	 * Populate a DefaultMutableTreeNode from a File object known as the "root" file (or directory).
	 * 
	 * Completed as the handin requires. May not be the ideal implementation for a FTP Program. Indeed, we are working on the same 
	 * machine from FTPServer to this CommandDispatcher, so when we transfer a File we have no problem. But if the Server and Client run
	 * on separated machines, we would need to build a TreeModel and populate it on the server-side. Due to a lack of time, we will keep 
	 * using the proposed method for the moment.
	 *  
	 * @param userRoot
	 * @param root
	 */
	private void populateTree(DefaultMutableTreeNode userRoot, File root) {
		File[] files = root.listFiles();
		if (files != null) {
		    for (File childFile : files) {
		    	DefaultMutableTreeNode childNode = new DefaultMutableTreeNode(childFile.getName());
		    	if (!childFile.isHidden())
		    		userRoot.add(childNode); 
		        if (childFile.isDirectory())
		        	populateTree(childNode, childFile); // Recursive call
		    }
		}
	}

	/**
	 * Sends a command to the FTPServer to ask what is the current working directory.
	 */
	private void pwd() {
		try {
			if (!alreadyConnected) {
				window.console.append(NL + "You are not connected to any server.");
				return;
			}
			String curDir = client.pwd();
			window.console.append(NL + "Current directory on FTP server: " + curDir);
			window.scrollPane.setBorder(new TitledBorder(new EtchedBorder(), "CWD: " + curDir)); // Added. More UI friendly
		} catch (IOException e) {
			window.console.append(NL + e.getMessage());
			e.printStackTrace();
		}
	}
	
	/**
	 * Added. Change the current working directory of the FTP.
	 * 
	 * Get the selected component in the FTPCLientWindow's JTree and call the SimpleFTP to send the request.
	 */
	private void cwd() {
		if (!alreadyConnected) {
			window.console.append(NL + "You are not connected to any server.");
			return;
		}
		// Method 1
		//String newDir =  window.tree.getLastSelectedPathComponent().toString();
		// Method 2
		TreePath path = window.tree.getSelectionPath();
		String newDir = "";
		Object[] pathComponents = path.getPath();
		int i=0;
		for (Object component : pathComponents) {
			if(i>0) 
				newDir += component+System.getProperty("file.separator");  
			i++;
		}
		
		try {
			if (client.cwd(newDir)) {
				window.console.append(NL + "Changed working directory to: "+ newDir);
				listDir();
				window.scrollPane.setBorder(new TitledBorder(new EtchedBorder(), "CWD: " + window.root.getRoot().toString()));
			}
			else
				window.console.append(NL + "Failed to change working directory.");
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	private void disconnect() {
		try {
			if (alreadyConnected) {				
				window.root = new DefaultMutableTreeNode("The FTP Server"); // Added. To reset the JTree.
				window.tree.setModel(new DefaultTreeModel(window.root));
				window.tree.updateUI();
				client.disconnect();
				alreadyConnected = false;
				// Added: enable buttons and TextFields
				window.tfusername.setEnabled(!alreadyConnected);
				window.tfservername.setEnabled(!alreadyConnected);
				window.tfport.setEnabled(!alreadyConnected);
				window.pfuserpass.setEnabled(!alreadyConnected);
				window.btnConnect.setEnabled(!alreadyConnected);
				window.console.append(NL + "Now disconnected");
			} 
			else 
				window.console.append(NL + "Already disconnected");
		} catch (IOException e) {
			window.console.append(NL + e.getMessage());
			e.printStackTrace();
		}

	}

	private void connect() {
		if (alreadyConnected) {
			window.console.append(NL
					+ "You are already connected to this server.");
			return;
		}
		server = window.tfservername.getText();
		if (server == null || "".equals(server)) {
			window.console.append(NL
					+ "You must specify the server IP address.");
			return;
		}
		port = window.tfport.getText();
		if (port == null || "".equals(port)) {
			window.console.append(NL
					+ "You must specify the server port I must connect to.");
			return;
		}
		user = window.tfusername.getText();
		if (user == null || "".equals(user))
			user = "anonymous";
		pass = new String(window.pfuserpass.getPassword());
		if (pass == null || "".equals(pass))
			pass = "anonymous";
		try {
			client.connect(server, Integer.parseInt(port), user, pass);
			alreadyConnected = true;
			// Added: disable buttons and TextFields. More user friendly.
			window.tfusername.setEnabled(!alreadyConnected);
			window.tfservername.setEnabled(!alreadyConnected);
			window.tfport.setEnabled(!alreadyConnected);
			window.pfuserpass.setEnabled(!alreadyConnected);
			window.btnConnect.setEnabled(!alreadyConnected);
			window.console.append(NL + "Now connected");
			pwd();
			listDir();
		} catch (IOException e) {
			window.console.append(NL + e.getMessage());
			e.printStackTrace();
		} catch (NumberFormatException nfe) {
			window.console.append(NL + nfe.getMessage());
			nfe.printStackTrace();
		}
	}
}
